查看原文
其他

【第1507期】从 loading 的 9 种写法谈 React 业务开发

ES2049 Studio 前端早读课 2019-08-03

前言

这是一篇比较全面讲解 React 的文章,里面很多基础知识希望你自己一边查阅资料一边学习。全文从业务开发中最常用见 loading 效果不同是实现讲起,说下现在前端开发在业务上应该有的思考。今日早读文章由前端早读课专栏作者,阿里@ES2049 Studio授权分享。

正文从这开始~~

入门级操作

State

最简单的实现,我们在 Loading 组件内部声明一个状态,通过代码逻辑判断 loading 效果的展示。

export default class extends Component {
 
...
 render
() {
   
return this.state.loading ?  : finish;
 
}
}

完整演示

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Loading from "./Loading/index";
import "./styles.scss";
class App extends Component {
 render
() {
   
return <Loading />;
 
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Props

随着业务的发展,这个 Loading 组件用到的地方会非常多,上面这个代码耦合了很多逻辑,为了让这个组件能够很好的复用,那我们抽离出组件的业务逻辑,将内部状态进行提升,那这个组件就是一个能被复用的 UI 组件。

export default function(props) {
 
return props.loading ?  : finish;
}

完整演示

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Loading from "./Loading/index";
import fetch from "./fetch";
import "./styles.scss";
class App extends Component {
 state
= {
   loading
: true
 
};
 componentDidMount
() {
   fetch
().then(() => {
     
this.setState(() => ({
       loading
: false
     
}));
   
});
 
}
 render
() {
   
return <Loading loading={this.state.loading} />;
 
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注:上面两段代码你可能会想,为什么 Func 和 Class 都能实现一个组件,他们有什么差别吗?
其实你在开发时不容易感觉到差别,但 React 本身是进行了很多差别处理,如果是 Class 类,React 会用 new 关键字实例化,然后调用该实例的 render 方法,如果是 Func 函数,React 会直接调用它。

Refs

如果你是一个 jQuery 转型 React 的开发,会很自然的想到,我找到 Loading 组件的节点,控制他的显示与隐藏,当然这也是可以的,React 提供 Refs 方便你访问 DOM 节点 或 React 元素。

export default class extends Component {
 componentDidMount
() {
   fetch
().then(() => {
     
this.el.changeLoading(false);
   
});
 
}
 render
() {
   
return (
     
{ this.el = el; }} />
   
);
 
}
}

完整实例

import React, { Component } from "react";
import ReactDOM from "react-dom";
import Loading from "./Loading/index";
import fetch from "./fetch";
import "./styles.scss";
class App extends Component {
 componentDidMount
() {
   fetch
().then(() => {
     
this.el.changeLoading(false);
   
});
 
}
 render
() {
   
return (
     
<Loading
       
ref={el => {
         
this.el = el;
       
}}
     
/>
   
);
 
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

通用逻辑抽离

当你的应用做到一定的复杂度,不同的页面都会有 loading 效果,你肯定不希望每个页面都重复的书写一样的逻辑,这样会导致你的代码重复且混乱。

React 中有两个比较常见的解决方案 HOC 和 Render Props,其实这两个这两个概念都是不依赖 React 的。

让我们暂时忘掉 React,下面我对 HOC 和 Render Props 写两个例子,你会发现组件复用是如此简单。

HOC

HOC 其实就是一种装饰器模式,它接受一个组件作为参数,然后返回相同的组件,这样就可以额外增加一些功能。

const func = () => {
 console
.log("func");
};
const wrap = func => {
 console
.log("wrap");
 
return func;
};
// wrap 逻辑已被复用
wrap
(func)();

完整实例

import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import "./styles.scss";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Render Props

Render Props 就是我们给一个函数传递一个回调函数做为参数,该回调函数就能利用外面函数的执行结果做为参数,执行任何操作。

const func = param => {
 console
.log("func");
};
const wrap = (param, func) => {
 console
.log("wrap");
 func
(param);
};
// wrap 逻辑已被复用
wrap
("", func);

完整实例

import React, { Component } from "react";
import ReactDOM from "react-dom";
import LoadingRender from "./LoadingRender";
import Loading from "./Loading/index";
import fetch from "./fetch";
import "./styles.scss";
class App extends Component {
 componentDidMount
() {
   fetch
().then(() => {
     
this.props.changeLoading(false);
   
});
 
}
 render
() {
   
return <Loading loading={this.props.loading} />;
 
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(
 
<LoadingRender render={props => <App {...props} />} />,
 rootElement
);

相同点:

  • 两者都能很好的帮助我们重用组件逻辑;

  • 和回调函数类似,当嵌套层数很多时,会造成回调地狱。

不同点:

  • HOC 和 父组件有相同属性名属性传递过来,会造成属性丢失;

  • Render Props 你只需要实例化一个中间类,而 HOC 你每次调用的地方都需要额外实例化一个中间类。

总的来说,在需要复用组件逻辑的时候,我个人更倾向于 Render Props 的方式。

复杂状态管理

当你的应用越来越大,组件之间交互越来越复杂,那整个页面的数据逻辑将变得难以管理,这时候为了方便管理应用的状态,你可以选择一些状态管理工具,例如 Redux、Flux、dva 等。

Redux

我不太想谈这些数据流框架,因为他们的概念 action、store、dispatch 太过于生涩难懂。

现代前端框架 React 和 Vue 其实都是一个套路,通过数据渲染试图,然后视图上操作反过来更新数据,重新渲染视图,刷新页面。

数据叫做 store,动作叫做 ation,触发行为叫 dispatch,然后数据到视图的渲染由 React/Vue 处理的。

// reducers.js
const initialState = {
 loading
: false
};
export default function reducer(state = initialState, action) {
 
switch (action.type) {
   
case "CHANGE_LOADING":
     
return {
       loading
: action.payload
     
};
   
default:
     
return state;
 
}
}

完整实例

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./app";
import "./styles.scss";
const rootElement = document.getElementById("root");
ReactDOM.render(
 
<Provider store={store}>
   
<App />
 
</Provider>,
 rootElement
);
Saga

当你代码中有大量的异步操作时,例如 fetch 请求,你肯定会想到事件监听、回调函数、发布/订阅。

很好,上一个例子其实就是事件监听的处理方式,然后回调函数的主流的解决方案是 redux-thunk,而发布/订阅的主流解决方案是 saga。

import { takeLatest, put } from "redux-saga/effects";
import fetch from "./fetch";
function* fetchInfo(action) {
 
yield put({
   type
: "CHANGE_LOADING",
   payload
: true
 
});
 
yield fetch();
 
yield put({
   type
: "CHANGE_LOADING",
   payload
: false
 
});
}
export default function* fetchSaga() {
 
yield takeLatest("FETCH_REQUEST", fetchInfo);
}

完整实例

import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import createSagaMiddleware from "redux-saga";
import reducers from "./reducers";
import fetchSaga from "./sagas";
import App from "./app";
import "./styles.scss";
const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducers, applyMiddleware(sagaMiddleware));
sagaMiddleware
.run(fetchSaga);
const rootElement = document.getElementById("root");
ReactDOM.render(
 
<Provider store={store}>
   
<App />
 
</Provider>,
 rootElement
);

当你耐心看到这里,我知道你对 React 肯定有一定的经验,现在还可以做很多,例如把 loading 状态提升到 Store 的顶部,那整个站点就只有一个 loading 了,然后你还可以将 fetch 再封装一个 HOC 修改 loading 状态,这就是一个相对完美的 loading,其实 React 业务开发都可以用这个套路。

新的 API

Context

上面 redux 的例子是不是过于复杂

对于简单的业务,虽然有很多页面,嵌套层次也很复杂,你当然可以不用状态管理工具,你可以试着使用 Context,它可以方便你传递数据,它其实就是 Render Props 的一种实现。

export default React.createContext({
 loading
: false,
 changeLoading
: () => {}
});

完整实例

import React, { Component } from "react";
import ReactDOM from "react-dom";
import LoadingContext from "./loading-context";
import Page from "./Page/index";
import "./styles.scss";
class App extends Component {
 state
= {
   loading
: true
 
};
 changeLoading
= param => {
   
this.setState({
     loading
: param
   
});
 
};
 render
() {
   
return (
     
<LoadingContext.Provider
       value
={{
         
...this.state,
         changeLoading
: this.changeLoading
       
}}
     
>
       
<Page />
     
</LoadingContext.Provider>
   
);
 
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hooks

写到这,静一下,是不是哪里做错了什么?

我的业务只是想写个简单的 loading 效果,却了解了一堆组件生命周期的概念。

Hooks 刚好帮你解决了这样的问题,Hooks 能允许你通过执行单个函数调用来使用函数中的 React 功能,让你把面向生命周期编程变成面向业务逻辑编程。

import React, { useState, useEffect } from "react";
import Loading from "./Loading/index";
import fetch from "./fetch";
function App() {
 
const [loading, setLoading] = useState(true);
 useEffect
(() => {
   fetch
().then(() => {
     setLoading
(false);
   
});
 
}, []);
 
return ;
}
export default App;

完整实例

import React from "react";
import ReactDOM from "react-dom";
import App from "./app";
import "./styles.scss";
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

好好总结

上面对每个点都做了具体实现,但他们都不是隔离的,你可以根据你的认知和业务特点总结抽象一套自己的方法论;

多了解、多抽象、多思考,练就十八般武艺,遇到问题的时候才能游刃有余;

React 现在宣传的东西越来越多,你最好先深入了解他们,然后用批判的眼光,保持理智,防止自己每天用很新的特性重构你自己的代码。

参考文章

  • React 官网

  • When do I know I’m ready for Redux?

关于本文
作者:@ES2049 Studio
原文:https://www.yuque.com/es2049/blog/xel32z

最后,为你推荐

【第1506期】JavaScript工程项目的一系列最佳实践策略

【第1505期】谈谈代理

【招聘】阿里云招P6+前端工程师

在公众号回复关键词 ES2049 即可查阅专栏作者所有文章

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存